<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="referrer" content="origin" />
    <meta http-equiv="Cache-Control" content="no-transform" />
    <meta http-equiv="Cache-Control" content="no-siteapp" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
    <title>基于WebSocket实现的系统资源监控(按Ctrl+Shift+S组合键保存数据到指定的目录)</title>
	<style type="text/css">
		#mask{display:none;}
		#wrapper{position:fixed;left:5px;top:5px;right:5px;bottom:5px;overflow:hidden;border:1px #999 solid;font-size:14px;}
		#wrapper canvas{position:absolute;z-index:2;}
		#wrapper > .proc, #procMask{position:absolute;left:-1px;top:40%;right:-1px;bottom:-1px;overflow:hidden;overflow-y:auto;border-top:1px #999 solid;}
		#wrapper > .proc caption{line-height:30px;font:normal 30px Arial;color:white;text-align:left;text-indent:5px;text-shadow:#999 0 0 2px,#999 0 1px 2px,#999 1px 0 2px,#999 1px 1px 2px;}
		#wrapper > .proc th{cursor:pointer;}
		#wrapper > .proc th.asc:after{content:'⇑';color:#999;}
		#wrapper > .proc th.desc:after{content:'⇓';color:#999;}
		#wrapper > .proc td{white-space:nowrap;}
		#wrapper > .proc table{border-collapse:collapse;}
		#wrapper > .proc tr > *{border:1px #ccc solid;}
		#wrapper > .proc tr div {width:40vw;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}
		#wrapper > .proc tbody > tr{cursor:pointer;}
		#wrapper > .proc tbody > tr:hover{background:#eee;}
		#wrapper > .proc > canvas {background:white;}

		#cpuCanvas,#diskCanvas{border-right:1px #999 solid;}
		#memCanvas,#netCanvas{border-left:1px #999 solid;}
		#loadavgCanvas{border-top:1px #999 solid;border-bottom:1px #999 solid;}

		#procMask{display:none;background:rgba(0, 0, 0, 0.6);border:0 none;}
		#procMask > div{position:absolute;left:25%;top:25%;right:25%;bottom:25%;background:white;}
		#procMask > canvas{left:25%;top:25%;}
		#procCanvas{cursor:pointer;}
		#dataLine,#dataView{position:absolute;display:none;}
		#dataLine{width:1px;background:#999;z-index:1;}
		#dataView{border:1px #999 solid;background:white;white-space:pre-wrap;padding:5px;z-index:3;}

		body.offline #mask{position:fixed;left:0;top:0;right:0;bottom:0;display:block;background:rgba(0, 0, 0, 0.6);}
	</style>
</head>
<body>
	<div id="wrapper">
		<canvas id="cpuCanvas" data="cpus"></canvas>
		<canvas id="memCanvas" data="mems"></canvas>
		<canvas id="loadavgCanvas" data="loadavgs"></canvas>
		<canvas id="diskCanvas" data="disks"></canvas>
		<canvas id="netCanvas" data="nets"></canvas>
		<div id="procBox" class="proc">
			<table width="100%" cellpadding="5px" cellspacing="0">
				<caption>PROC</caption>
				<thead>
					<tr id="procHead">
						<th>pid</th>
						<th>ppid</th>
						<th>pgrp</th>
						<th>state</th>
						<th>threads</th>
						<th>fds</th>
						<th format="fsize">resident</th>
						<th format="fsize">rssFile</th>
						<th>ucpu</th>
						<th>scpu</th>
						<th class="desc">tcpu</th>
						<th format="strtotime">etime</th>
						<th format="fsize">read_bytes</th>
						<th format="fsize">write_bytes</th>
						<th>comm</th>
						<th>cmdline</th>
					</tr>
				</thead>
				<tbody id="procList">
				</tbody>
			</table>
		</div>
		<div id="procMask">
			<div></div>
			<canvas id="procCanvas" data="procInfos" title="Click here close proc monitor."></canvas>
		</div>
		<div id="dataLine"></div>
		<div id="dataView"></div>
	</div>
	<div id="mask"></div>

	<script type="text/javascript">
	const wrapper = document.getElementById('wrapper');
	const cpuElem = document.getElementById('cpuCanvas');
	const cpuCanvas = cpuElem.getContext('2d');
	const memElem = document.getElementById('memCanvas');
	const memCanvas = memElem.getContext('2d');
	const loadavgElem = document.getElementById('loadavgCanvas');
	const loadavgCanvas = loadavgElem.getContext('2d');
	const diskElem = document.getElementById('diskCanvas');
	const diskCanvas = diskElem.getContext('2d');
	const netElem = document.getElementById('netCanvas');
	const netCanvas = netElem.getContext('2d');
	const procBox = document.getElementById('procBox');
	const procMask = document.getElementById('procMask');
	const procElem = document.getElementById('procCanvas');
	const procCanvas = procElem.getContext('2d');
	const procHead = document.getElementById('procHead');
	const procList = document.getElementById('procList');
	const dataLine = document.getElementById('dataLine');
	const dataView = document.getElementById('dataView');
	let ws = null;
	const strtotime = function(i) {
		const sec = parseInt(i) % 60;
		const min = parseInt(i / 60) % 60;
		const hour = parseInt(i / 3600) % 24;
		const day = parseInt(i / 86400);

		return (day > 0 ? day + '-' : '') + (hour < 10 ? '0' : '') + hour + ':' + (min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec;
	};
	const fsize = function(size) {
		if(size <= 0) {
			return '0';
		}
		
		let unit = parseInt(Math.log(size)/Math.log(1024));
		if(unit > 8) {
			unit = 8;
		}
		if(unit < 0) unit = 0;

		return Math.round(size / Math.pow(1024, unit), 1) + "0KMGTPEZY".charAt(unit);
	};
	const formats = {
		strtotime: strtotime,
		fsize: fsize
	};
	const actions = {
		cpus: [],
		cpuColor: {
			user: '#A52A2A',
			nice: '#8A2BE2',
			system: '#0000FF',
			idle: '#7FFF00',
			iowait: '#DEB887',
			irq: '#5F9EA0',
			softirq: '#D2691E',
			stolen: '#FF7F50',
			guest: '#6495ED'
		},
		mems: [],
		memColor: {
			free: '#A52A2A',
			buffers: '#8A2BE2',
			cached: '#0000FF',
			locked: '#7FFF00',
			shared: '#6495ED',
			swapFree: '#5F9EA0'
		},
		loadavgs: [],
		loadavgColor: {
			min1: '#A52A2A',
			min5: '#8A2BE2',
			min15: '#0000FF',
			runs: '#7FFF00',
			procs: '#6495ED'
		},
		disks: [],
		diskColor: {
			rd_bytes: '#A52A2A',
			rd_ops: '#8A2BE2',
			wr_bytes: '#0000FF',
			wr_ops: '#7FFF00'
		},
		nets: [],
		netColor: {
			recv_bytes: '#A52A2A',
			recv_packets: '#8A2BE2',
			send_bytes: '#0000FF',
			send_packets: '#7FFF00'
		},
		procs: [],
		procInfos: [],
		procColor: {
			threads: '#A52A2A',
			fds: '#8A2BE2',
			resident: '#0000FF',
			rssFile: '#7FFF00',
			ucpu: '#DEB887',
			scpu: '#5F9EA0',
			tcpu: '#6495ED',
			read_bytes: '#D2691E',
			write_bytes: '#FF7F50'
		},
		init: function() {
			this.firstPid = true;
		},
		cpuGraphic: function(cpu) {
			const width = cpuElem.clientWidth;
			const height = cpuElem.clientHeight;
			cpuCanvas.reset();
			cpuCanvas.clearRect(0, 0, width, height);

			while(this.cpus.length > width / 2) {
				this.cpus.shift(0);
			}

			updateData('cpus');

			const keys = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'stolen', 'guest'];
			const x = width - this.cpus.length * 2;

			keys.forEach(function(key) {
				cpuCanvas.beginPath();
				actions.cpus.forEach(function(cpu, i) {
					const y = height - height * cpu[key] / 100.0;
					if(i == 0) {
						cpuCanvas.moveTo(x, y);
					} else {
						cpuCanvas.lineTo(x + i * 2, y);
					}
				});
				cpuCanvas.lineWidth = 1;
				cpuCanvas.strokeStyle = actions.cpuColor[key];
				cpuCanvas.lineJoin = "miter";
				cpuCanvas.stroke();
				cpuCanvas.closePath();
			});

			cpuCanvas.font = 'normal bold 30px Arial';
			{
				const str = "CPU";
				const sz = cpuCanvas.measureText(str);
				cpuCanvas.clearRect(5, 0, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				cpuCanvas.strokeStyle = '#999999';
				cpuCanvas.strokeText(str, 5, sz.fontBoundingBoxAscent);
			}

			if(isNaN(cpu) && this.cpus.length > 0) cpu = this.cpus[this.cpus.length-1];
			else return;

			const h = (height - 45) / keys.length;

			keys.forEach(function(key, i) {
				const str = key + ': ' + cpu[key] + '%';
				const x = 5, y = 40 + (i + 1) * h;
				
				cpuCanvas.font = 'normal bold ' + h + 'px Arial';
				{
					const sz = cpuCanvas.measureText(str);
					cpuCanvas.clearRect(x, y - sz.fontBoundingBoxAscent, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				}
				cpuCanvas.fillStyle = actions.cpuColor[key];
				cpuCanvas.fillText(str, x, y);
			});
		},
		cpu: function(cpu) {
			this.cpus.push(cpu);
			this.cpuGraphic(cpu);
		},
		memGraphic: function(mem) {
			const width = memElem.clientWidth;
			const height = memElem.clientHeight;
			memCanvas.reset();
			memCanvas.clearRect(0, 0, width, height);

			while(this.mems.length > width / 2) {
				this.mems.shift(0);
			}

			updateData('mems');

			const keys = ['free', 'buffers', 'cached', 'locked', 'shared', 'swapFree'];
			const x = width - this.mems.length * 2;

			keys.forEach(function(key) {
				memCanvas.beginPath();
				actions.mems.forEach(function(mem, i) {
					const y = height - height * mem[key] / (key === 'swapFree' ? mem.swapTotal : mem.total);
					if(i == 0) {
						memCanvas.moveTo(x, y);
					} else {
						memCanvas.lineTo(x + i * 2, y);
					}
				});
				memCanvas.lineWidth = 1;
				memCanvas.strokeStyle = actions.memColor[key];
				memCanvas.lineJoin = "miter";
				memCanvas.stroke();
				memCanvas.closePath();
			});

			memCanvas.font = 'normal bold 30px Arial';
			{
				const str = "Memory";
				const sz = memCanvas.measureText(str);
				memCanvas.clearRect(5, 0, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				memCanvas.strokeStyle = '#999999';
				memCanvas.strokeText(str, 5, sz.fontBoundingBoxAscent);
			}

			if(isNaN(mem) && this.mems.length > 0) mem = this.mems[this.mems.length-1];
			else return;

			const h = (height - 45) / keys.length;

			keys.forEach(function(key, i) {
				const str = key + ': ' + fsize(mem[key]) + (key === 'free' ? ' / ' + fsize(mem.total) : (key === 'swapFree' ? ' / ' + fsize(mem.swapTotal) : ''));
				const x = 5, y = 40 + (i + 0.5) * h;

				memCanvas.font = 'normal bold ' + (h * 2 / 3) + 'px Arial';
				{
					const sz = memCanvas.measureText(str);
					memCanvas.clearRect(x, y - sz.fontBoundingBoxAscent, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				}
				memCanvas.fillStyle = actions.memColor[key];
				memCanvas.fillText(str, x, y);
			});
		},
		mem: function(mem) {
			this.mems.push(mem);
			this.memGraphic(mem);
		},
		loadavgGraphic: function(la) {
			const width = loadavgElem.clientWidth;
			const height = loadavgElem.clientHeight;
			loadavgCanvas.reset();
			loadavgCanvas.clearRect(0, 0, width, height);

			while(this.loadavgs.length > width / 2) {
				this.loadavgs.shift(0);
			}
			
			updateData('loadavgs');

			const max = {min1: 1, min5: 1, min15: 1, runs: 0, procs: 0};
			const keys = Object.keys(max);
			const x = width - this.loadavgs.length * 2;

			this.loadavgs.forEach(function(la) {
				keys.forEach(function(key) {
					max[key] = Math.max(max[key], la[key]);
				});
			});
			keys.forEach(function(key) {
				loadavgCanvas.beginPath();
				actions.loadavgs.forEach(function(la, i) {
					const y = height - height * la[key] / max[key];
					if(i == 0) {
						loadavgCanvas.moveTo(x, y);
					} else {
						loadavgCanvas.lineTo(x + i * 2, y);
					}
				});
				loadavgCanvas.lineWidth = 1;
				loadavgCanvas.strokeStyle = actions.loadavgColor[key];
				loadavgCanvas.lineJoin = "miter";
				loadavgCanvas.stroke();
				loadavgCanvas.closePath();
			});

			loadavgCanvas.font = 'normal bold 30px Arial';
			{
				const str = "Loadavg";
				const sz = loadavgCanvas.measureText(str);
				loadavgCanvas.clearRect(5, 0, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				loadavgCanvas.strokeStyle = '#999999';
				loadavgCanvas.strokeText(str, 5, sz.fontBoundingBoxAscent);
			}

			if(isNaN(la) && this.loadavgs.length > 0) la = this.loadavgs[this.loadavgs.length-1];
			else return;

			const h = (height - 35) / keys.length;

			keys.forEach(function(key, i) {
				const str = key + ': ' + la[key];
				const x = 5, y = 35 + (i + 0.6) * h;
				loadavgCanvas.font = 'normal bold ' + (h*0.8) + 'px Arial';
				{
					const sz = loadavgCanvas.measureText(str);
					loadavgCanvas.clearRect(x, y - sz.fontBoundingBoxAscent, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				}
				loadavgCanvas.fillStyle = actions.loadavgColor[key];
				loadavgCanvas.fillText(str, x, y);
			});

			loadavgCanvas.font = 'normal bold ' + (h*0.8) + 'px Arial';
			{
				const str = 'runs: ' + la.runs + ', procs: ' + la.procs;
				const sz = loadavgCanvas.measureText(str);
				loadavgCanvas.clearRect(140, 30 - sz.fontBoundingBoxAscent, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				loadavgCanvas.fillStyle = 'red';
				loadavgCanvas.fillText(str, 140, 30);
			}
		},
		loadavg: function(la) {
			this.loadavgs.push(la);
			this.loadavgGraphic(la);
		},
		pid: 0,
		comm: '',
		procOrder: {
			by: 'tcpu',
			asc: false
		},
		procClick: function(th) {
			if(this.procOrder.by === th.innerText) {
				this.procOrder.asc = !this.procOrder.asc;
			} else {
				this.procOrder.asc = false;
				this.procOrder.by = th.innerText;
			}
			const key = this.procOrder.by;
			const asc = this.procOrder.asc;
			Object.values(procHead.getElementsByTagName('th')).forEach(function(th) {
				const key = th.innerText;

				th.removeAttribute('class');
			});
			th.setAttribute('class', asc ? 'asc' : 'desc');
			this.procSort();
			this.procGraphic();
		},
		procSort: function() {
			const key = this.procOrder.by;
			const asc = this.procOrder.asc;
			this.procs.sort(function(a, b) {
				let cmp;
				if(typeof(a[key]) === 'string') {
					cmp = a[key].localeCompare(b[key]);
				} else if(typeof(b[key]) === 'string') {
					cmp = - b[key].localeCompare(a[key]);
				} else if(a[key] > b[key]) {
					cmp = 1;
				} else if(a[key] < b[key]) {
					cmp = -1;
				} else {
					cmp = 0;
				}

				if(asc) return cmp;
				else return -cmp;
			});
		},
		procGraphic: function() {
			procList.innerHTML = '';
			actions.procs.forEach(function(proc) {
				const tr = document.createElement('tr');
				procList.appendChild(tr);
				tr.onclick = function() {
					procMask.style.display = 'block';
					if(actions.pid != proc.pid) {
						actions.procInfos = [];
					}
					actions.pid = proc.pid;
					actions.comm = proc.comm;
					actions.procInfos.push(proc);
					actions.procInfoGraphic();
				};
				actions.procColumns.forEach(function(col, i) {
					const td = document.createElement('td');
					tr.appendChild(td);

					if(i == actions.procColumns.length - 1) {
						const div = document.createElement('div');
						td.appendChild(div);
						div.innerText = col.format(proc[col.key]);
					} else {
						td.innerText = col.format(proc[col.key]);
					}
				});
			});

			this.procInfoGraphic();
		},
		procInfoGraphic: function() {
			const width = procElem.clientWidth;
			const height = procElem.clientHeight;
			procCanvas.reset();
			procCanvas.clearRect(0, 0, width, height);

			if(this.pid <= 0) return;

			while(this.procInfos.length > width / 2) {
				this.procInfos.shift(0);
			}

			const max = {threads: 50, fds: 10, resident: 1, rssFile: 1, ucpu: 100, scpu: 100, tcpu: 100, read_bytes: 1024*1024, write_bytes: 1024*1024};
			const keys = Object.keys(max);
			const x = width - this.procInfos.length * 2;

			this.procInfos.forEach(function(proc) {
				keys.forEach(function(key) {
					max[key] = Math.max(max[key], proc[key]);
				});
			});
			keys.forEach(function(key) {
				procCanvas.beginPath();
				actions.procInfos.forEach(function(proc, i) {
					const y = height - height * proc[key] / max[key];
					if(i == 0) {
						procCanvas.moveTo(x, y);
					} else {
						procCanvas.lineTo(x + i * 2, y);
					}
				});
				procCanvas.lineWidth = 1;
				procCanvas.strokeStyle = actions.procColor[key];
				procCanvas.lineJoin = "miter";
				procCanvas.stroke();
				procCanvas.closePath();
			});

			procCanvas.font = 'normal bold 30px Arial';
			{
				const str = this.pid + ' - ' + this.comm;
				const sz = procCanvas.measureText(str);
				procCanvas.clearRect(5, 0, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				procCanvas.strokeStyle = '#999999';
				procCanvas.strokeText(str, 5, sz.fontBoundingBoxAscent);
			}

			const proc = this.procInfos[this.procInfos.length-1];

			const h = (height - 50) / keys.length;

			keys.forEach(function(key, i) {
				const str = key + ': ' + ((key.endsWith('bytes') || key === 'rssFile' || key === 'resident') ? fsize(proc[key]) : proc[key]);
				const x = 5, y = 45 + (i + 0.5) * h;
				procCanvas.font = 'normal bold ' + (h/2) + 'px Arial';
				{
					const sz = procCanvas.measureText(str);
					procCanvas.clearRect(x, y - sz.fontBoundingBoxAscent, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				}
				procCanvas.fillStyle = actions.procColor[key];
				procCanvas.fillText(str, x, y);
			});
		},
		firstPid: true,
		proc: function(procs, pid) {
			if(this.firstPid) {
				this.firstPid = false;
				if(this.pid <= 0 || !(this.pid in procs)) {
					this.pid = pid;
					this.comm = procs[pid].comm;
					procMask.style.display = 'block';
				}
			}
			this.procs = Object.values(procs);
			this.procs.forEach(function(proc) {
				proc.tcpu = proc.ucpu + proc.scpu;
			});
			if(this.pid > 0 && (this.pid in procs)) {
				this.procInfos.push(procs[this.pid]);
				updateData('procInfos');
			} else {
				this.pid = 0;
				this.procInfos = [];
				procMask.style.display = 'none';
				procCanvas.reset();
			}
			this.procSort();
			this.procGraphic();
		},
		diskGraphic: function(disk) {
			const width = diskElem.clientWidth;
			const height = diskElem.clientHeight;
			diskCanvas.reset();
			diskCanvas.clearRect(0, 0, width, height);

			while(this.disks.length > width / 2) {
				this.disks.shift(0);
			}

			updateData('disks');

			const max = {rd_bytes: 1024*1024, rd_ops: 100, wr_bytes: 1024*1024, wr_ops: 100};
			const keys = Object.keys(max);
			const x = width - this.disks.length * 2;

			this.disks.forEach(function(disk) {
				keys.forEach(function(key) {
					max[key] = Math.max(max[key], disk[key]);
				});
			});
			keys.forEach(function(key) {
				diskCanvas.beginPath();
				actions.disks.forEach(function(disk, i) {
					const y = height - height * disk[key] / max[key];
					if(i == 0) {
						diskCanvas.moveTo(x, y);
					} else {
						diskCanvas.lineTo(x + i * 2, y);
					}
				});
				diskCanvas.lineWidth = 1;
				diskCanvas.strokeStyle = actions.diskColor[key];
				diskCanvas.lineJoin = "miter";
				diskCanvas.stroke();
				diskCanvas.closePath();
			});

			diskCanvas.font = 'normal bold 30px Arial';
			{
				const str = "DISK";
				const sz = diskCanvas.measureText(str);
				diskCanvas.clearRect(5, 0, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				diskCanvas.strokeStyle = '#999999';
				diskCanvas.strokeText(str, 5, sz.fontBoundingBoxAscent);
			}

			if(isNaN(disk) && this.disks.length > 0) disk = this.disks[this.disks.length-1];
			else return;

			const h = (height - 50) / keys.length;

			keys.forEach(function(key, i) {
				const str = key + ': ' + (key.endsWith('bytes') ? fsize(disk[key]) : disk[key]);
				const x = 5, y = 40 + (i + 0.8) * h;
				diskCanvas.font = 'normal bold ' + (h*0.8) + 'px Arial';
				{
					const sz = diskCanvas.measureText(str);
					diskCanvas.clearRect(x, y - sz.fontBoundingBoxAscent, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				}
				diskCanvas.fillStyle = actions.diskColor[key];
				diskCanvas.fillText(str, x, y);
			});
		},
		disk: function(disk) {
			const total = {rd_bytes: 0, rd_ops: 0, wr_bytes: 0, wr_ops: 0};
			const keys = Object.keys(total);
			Object.keys(disk).forEach(function(k) {
				const d = disk[k];
				if(!/^([sh]d[a-z]+|mmcblk\d+p)\d+$/.test(k)) {
					keys.forEach(function(key) {
						total[key] += d[key];
					});
				}
			});
			total.disk = disk;
			this.disks.push(total);
			this.diskGraphic(total);
		},
		netGraphic: function(net) {
			const width = netElem.clientWidth;
			const height = netElem.clientHeight;
			netCanvas.reset();
			netCanvas.clearRect(0, 0, width, height);

			while(this.nets.length > width / 2) {
				this.nets.shift(0);
			}

			updateData('nets');

			const max = {recv_bytes: 1024*1024, recv_packets: 100, send_bytes: 1024*1024, send_packets: 100};
			const keys = Object.keys(max);
			const x = width - this.nets.length * 2;

			this.nets.forEach(function(net) {
				keys.forEach(function(key) {
					max[key] = Math.max(max[key], net[key]);
				});
			});
			keys.forEach(function(key) {
				netCanvas.beginPath();
				actions.nets.forEach(function(net, i) {
					const y = height - height * net[key] / max[key];
					if(i == 0) {
						netCanvas.moveTo(x, y);
					} else {
						netCanvas.lineTo(x + i * 2, y);
					}
				});
				netCanvas.lineWidth = 1;
				netCanvas.strokeStyle = actions.netColor[key];
				netCanvas.lineJoin = "miter";
				netCanvas.stroke();
				netCanvas.closePath();
			});

			netCanvas.font = 'normal bold 30px Arial';
			{
				const str = "NET";
				const sz = netCanvas.measureText(str);
				netCanvas.clearRect(5, 0, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				netCanvas.strokeStyle = '#999999';
				netCanvas.strokeText(str, 5, sz.fontBoundingBoxAscent);
			}

			if(isNaN(net) && this.nets.length > 0) net = this.nets[this.nets.length-1];
			else return;

			const h = (height - 50) / keys.length;

			keys.forEach(function(key, i) {
				const str = key + ': ' + (key.endsWith('bytes') ? fsize(net[key]) : net[key]);
				const x = 5, y = 40 + (i + 0.8) * h;
				netCanvas.font = 'normal bold ' + (h*0.8) + 'px Arial';
				{
					const sz = netCanvas.measureText(str);
					netCanvas.clearRect(x, y - sz.fontBoundingBoxAscent, sz.width, sz.fontBoundingBoxAscent + sz.fontBoundingBoxDescent);
				}
				netCanvas.fillStyle = actions.netColor[key];
				netCanvas.fillText(str, x, y);
			});
		},
		net: function(net) {
			const total = {recv_bytes: 0, recv_packets: 0, send_bytes: 0, send_packets: 0};
			const keys = Object.keys(total);
			Object.keys(net).forEach(function(k) {
				const n = net[k];
				keys.forEach(function(key) {
					total[key] += n[key];
				});
			});
			total.net = net;
			this.nets.push(total);
			this.netGraphic(total);
		}
	};
	const WS = function() { // WebSocket connect
		ws = new WebSocket('ws://' + location.host + '/default/ws-monitor' + (location.search == '' ? '?index=0' : location.search));
		ws.onopen = function(e) {
			document.body.className = 'online';
		};
		ws.onerror = function(e) {
			console.clear();
			console.error(e);
		};
		ws.onmessage = function(e) {
			const json = JSON.parse(e.data);

			actions[json.action](json.data, json.pid);
		};
		ws.onclose = function(e) {
			document.body.className = 'offline';
			ws = null;

			setTimeout(WS, 1000);
		};
	};
	let dirHandle = false;
	async function save2File(name, data) {
		if(!dirHandle) return;

		try {
			const fd = await dirHandle.getFileHandle(name, {create: true});
			const f = await fd.createWritable();
			await f.write(JSON.stringify(data, null, 4));
			await f.close();
		} catch(e) {
			console.error(e);
			dirHandle = false;
		}
	};
	if(window.showDirectoryPicker) {
		document.body.onkeydown = function(e) {
			if(!e.ctrlKey || !e.shiftKey || e.key != 'S') return;

			window.showDirectoryPicker().then(function(dir) {
				dirHandle = dir;
			});
		};
	}
	document.body.onload = function() {
		this.onresize();
	};
	document.body.onresize = function() {
		const width = wrapper.clientWidth;
		const height = parseInt(wrapper.clientHeight * 0.4);
		let w = parseInt(width / 2);
		let h1 = parseInt(height * 2 / 5);
		let h2 = (height - h1) / 2;
		cpuElem.style.left = 0;
		cpuElem.width = w;
		cpuElem.height = h1;
		memElem.style.right = 0;
		memElem.width = width - w - 1;
		memElem.height = h1;
		loadavgElem.style.top = h1 + 'px';
		loadavgElem.width = width;
		loadavgElem.height = h2 - 1;
		diskElem.style.left = 0;
		diskElem.style.top = h1 + h2 + 'px';
		diskElem.width = w;
		diskElem.height = h2;
		netElem.style.right = 0;
		netElem.style.top = h1 + h2 + 'px';
		netElem.width = width - w - 1;
		netElem.height = h2;

		{
			const height = procBox.clientHeight;

			procElem.width = Math.ceil(width / 2) + 1;
			procElem.height = height / 2;
		}

		actions.cpuGraphic();
		actions.memGraphic();
		actions.loadavgGraphic();
		actions.diskGraphic();
		actions.netGraphic();
	};
	procElem.onclick = function() {
		actions.pid = 0;
		actions.procInfos = [];
		procMask.style.display = 'none';
		procCanvas.reset();
	};
	const formatData = function(data, sep) {
		const ret = [];
		
		Object.keys(data).forEach(function(key) {
			const val = data[key];
			if(typeof(val) === 'object') {
				ret.push(key + ':');
				Object.keys(val).forEach(function(k) {
					ret.push('    ' + k + ': ' + formatData(val[k], ', '));
				});
			} else {
				ret.push(key + ': ' + val);
			}
		});
		
		return ret.join(sep || '\n');
	};
	const dataXs = {key: false};
	const updateData = function(key) {
		const data = actions[key];

		save2File(key + '.json', data);

		if(dataXs.key != key) return;

		const X = dataXs[key].width - data.length * 2;
		const x = parseInt((dataXs[key].x - X) / 2);

		if(x >= 0 && x < data.length) {
			dataLine.style.display = 'block';
			dataView.style.display = 'block';
			dataView.innerText = formatData(data[x]);
		} else {
			dataLine.style.display = 'none';
			dataView.style.display = 'none';
		}
	};
	const mouseMoveEvent = function(e) {
		const width = this.clientWidth;
		const height = this.clientHeight;
		const key = this.getAttribute('data');
		const data = actions[key];
		const X = width - data.length * 2;
		const x = parseInt((e.offsetX - X) / 2);

		dataXs[key] = {width: width, x: e.offsetX};
		dataXs.key = key;

		dataLine.style.left = e.pageX - wrapper.offsetLeft - 1 + 'px';
		dataLine.style.top = e.pageY - e.offsetY - wrapper.offsetTop - 1 + 'px';
		dataLine.style.height = height + 'px';

		if(e.pageX > window.innerWidth / 2) {
			dataView.style.left = 'auto';
			dataView.style.right = window.innerWidth - e.pageX + 'px';
		} else {
			dataView.style.left = e.pageX + 'px';
			dataView.style.right = 'auto';
		}
		if(e.pageY > window.innerHeight / 2) {
			dataView.style.top = 'auto'
			dataView.style.bottom = window.innerHeight - e.pageY + 'px';
		} else {
			dataView.style.top = e.pageY + 'px';
			dataView.style.bottom = 'auto';
		}

		if(x >= 0 && x < data.length) {
			dataLine.style.display = 'block';
			dataView.style.display = 'block';

			dataView.innerText = formatData(data[x]);
		} else {
			dataLine.style.display = 'none';
			dataView.style.display = 'none';
		}
	};
	const mouseLeaveEvent = function() {
		dataLine.style.display = 'none';
		dataView.style.display = 'none';
		dataXs.key = false;
	};
	Object.values(document.getElementsByTagName('canvas')).forEach(function(elem) {
		elem.onmousemove = mouseMoveEvent;
		elem.onmouseleave = mouseLeaveEvent;
	});
	actions.procColumns = [];
	Object.values(procHead.getElementsByTagName('th')).forEach(function(th) {
		const key = th.innerText;
		const format = th.getAttribute('format');

		actions.procColumns.push({key:key, format:formats[format]||function(a){return a;}});
		th.onclick = function() {
			actions.procClick(this);
		};
	});

	setTimeout(WS, 50); // auto connect
	</script>
</body>
</html>
